Arc Post-Mortem Summary. This arc rebuilt the stylesheets' foundation and made pages answer the width they are given. It named the repeated colors, lengths, and control heights as custom properties (I), said lengths in rem and clamp rather than inches (II), and used logical direction properties so right-to-left falls out for free (III). It moved the phone-versus-desktop split from a user-agent guess into width-based media queries and added a short-height media query so a phone in landscape gets the compact layout too (IV), and added a built-in light/dark mode following the visitor's preference along with reduced-motion and focus-visible accessibility handling (V). It closed the interface fixes handed over from Phase 4.5 — the resource toolbar's uneven control heights, its layout on a phone, and the Add Search dialog (VI). Alongside the planned work it fixed a run of live-site issues: a large-folder listing timeout, a wiki title, several mail-server out-of-memory paths, a served-static-folder whole-file read, and the static-folder directory-index links, and it added a memory-fatal log that names the request the always-on server dies on. Two follow-ups were settled as not worth doing: an automated accessibility checker beside the existing one, and a visitor-facing theme picker.
Arc started 2026-07-17, after Phase 4.5. Sections are Roman numerals and steps within them are lettered; each step's title says what it does, followed by a sentence on what was proposed and, once worked, a few on what was done and anything learned. Marker legend: ▶ being worked on, ⚑ implemented and waiting to be reviewed, ✓ done, ✓✗ closed by deciding not to do it, ? open decision, unmarked = planned.
Yioop decides how it looks in two places at once. The server reads the
user agent and branches on $_SERVER["MOBILE"] in 101 places
across 45 files, sending different markup to a phone; the stylesheets then
branch again on a .mobile class in 290 places. A page therefore
cannot answer the width it is actually given, only the width the server
guessed from a name. The viewport meta tag that would let a phone say how
wide it is is emitted only when the server has already decided the reader is
on one (WebLayout.php:100), because emitting it always was tried
and reverted: the layout is sized in inches and points and runs off the side
of a phone when the tag is honoured.
So the units come first, then the layout can flex, then the tag can be told the truth, and then the branches can go. That order is why item 25 was moved out of Phase 0 and why it sits at the end here rather than the start. The measurements below were taken on 2026-07-17 at the start of the arc; the master to-do's figures predate several arcs and read lower.
| What | search.css | all css |
|---|---|---|
lengths in in | 309 | 340 |
lengths in pt | 138 | 169 |
lengths in px | 1171 | 1736 |
lengths in rem or em | 32 | 33 |
.html-ltr / .html-rtl selectors |
110 | 124 |
| logical properties already used | — | 18 |
.mobile selectors | — | 290 |
| custom properties defined | 0 | 0 |
| distinct hex colors | — | 111 |
Master to-do item 26. Foundational: items 9, 17, 18 consume these, and sections II and IV below cannot be done tidily without them.
:root and
replace the literals. Four parts:
--gray-0 to
--gray-100, numbered by lightness, replacing 132
literals in search.css.black,
white, gray and their kin in search.css,
and 48 more with 139 hex literals across mail.css, messages.css,
frise.css and editor.css, which read the same ladder.#e9e9ed button face stayed literals rather than
being flattened, which a pixel comparison caught a first attempt
doing.--font-size-1 through
--font-size-7, smallest first, with
--font-size-base naming the 12pt that body text is set
in and that a browser would have chosen anyway. 160 declarations
across the five stylesheets now read them, leaving 19 rare sizes as
literals. Six pages render identically.--space-1: 1px would rename a number to a number, and
snapping the odd steps onto an even ladder would move 122 pieces of
layout for tidiness alone.:root instead of 160 declarations, which is the whole
reason this section comes before section II.--control-height
now names it: how tall a thing you can press or type into stands, an
icon button, a select, a search field. The eight are the icon button
and its container, the button group, the filter field on a phone once
it has focus, and four of the git issue controls; the one 32px left is
a glyph's size, which is not a control. Six pages render identically,
and section VI.A can now say what it means when it makes the resource
toolbar one row of one height.Master to-do item 29. A phone cannot honour an inch; this is what made the viewport meta tag unusable.
rem, em, %,
ch and vw. Every inch across the
stylesheets is now rem, at six rem to the inch, which is
an inch's ninety-six pixels over a rem's sixteen: 309 in search.css and
31 more in the editor, frise, mail and messages sheets. That ratio
keeps the rendered size exactly as it was at the default type size
while letting it grow when a reader enlarges their text, which is the
point of the change; six pages render to the pixel. The points went
with the type scale in I.B, save the scale's own defining values in
:root, which stay points because that is where the sizes
are set. The wide page-content widths are in rem like the
rest; whether some should instead be a share of the viewport is II.C's
question, left open there rather than pre-empted here.clamp(). Two things now grow
with a wide screen and hold still on a normal one, bounded so neither
runs away:
clamp() of its present
size as a floor, a middle that grows once the viewport passes an
onset of 1100px, and a ceiling a few points above. Body text is 16px
at 1100px and below, 17.7px at 1440px, and 20px from 1920px up.--fluid-onset (the
1100px width below which nothing changes) and --fluid-rate
(how fast a size grows past it). At and below the onset every value
equals what it was, so a normal window is untouched; the growth is on
wider screens. This is the first Phase 5 change that alters rendering
by design rather than holding it identical, so the static renderer here
cannot check it — wkhtmltoimage does not resolve
vw against a set width — and the sizes were verified
by arithmetic across viewports rather than by pixel diff. It wants a
browser's eye at a few widths.px: they draw the interface's lines and edges, which
should not thicken when a reader enlarges their text.rem at sixteen pixels to
the rem, so the space around and between things grows with the
reader's type. 799 lengths in search.css moved this way, every one
dividing cleanly since sixteen is a power of two, so the size is
identical at the default and six pages render to the pixel.px: they are
hairline nudges that align an edge, not space that should scale, and
rounding them into rem would move them by a rem's rounding rather
than hold the line they were placed on.Master to-do item 27. Independent of I and II, and can be worked in parallel or dropped without affecting them.
.html-ltr/.html-rtl rules with logical
properties. Across the stylesheets the mirror pairs are now
single rules: where left-to-right said float: left and
right-to-left said float: right, one rule says
float: inline-start, and the same for
margin-inline-start, padding-inline-end,
inset-inline-start, text-align: start and the
rest. 43 pairs collapsed in search.css and six more in mail.css and
messages.css, the last two a directional border radius named with the
logical corner properties (border-end-end-radius and its
siblings); editor.css and frise.css had none.
.html-ltr and
.html-rtl selectors in its list rather than dropping the
prefix. Dropping it lowers the rule's specificity by a class and lets
other rules win that did not before, which broke the layout when
first tried; keeping both holds the specificity exactly while the
logical property does the direction.Master to-do items 23 and 25. This is the point of the arc; I and II exist so that this can be done without the layout running off the side.
$_SERVER["MOBILE"] branches into media queries.
108 branch sites across 49 PHP files (the earlier 101/45 count was
close). File by file, and each one is a decision about markup as much
as CSS: a branch that emits different content is not the same
as one that emits a different arrangement, and only the second
can become a media query. Too large for one patch, so in substeps:
@media (max-width: 600px) block with four utilities:
hide-narrow (a wide-only column or cell, always emitted
and hidden below 600px, with the spacer-row colspan
made the constant desktop count), show-narrow (a
phone-only <br> or clear div, hidden by default
and shown below 600px), center-wide (centered only
above 600px), and a configure-lang-shift class for the
one language-label offset. The column-hides across Manageclassifiers,
Managelocales, Crawlstatus, Manageusers, Managegroups,
Manageadvertisements, Searchsources, and Wiki resources became CSS,
as did the clear divs, footer and result <br>s,
the center swaps, and the configure nudge. Doing the work moved a
set of sites out of arrangement: a branch that emits different
markup — a <br> versus a
</td><td> cell break, or a
</tr><tr> row-break in the classifier and
locale edit forms — cannot become a media query without first
making the table one shape, so those stay PHP, and so does the
pagination gap-versus-full-list. The triage records the final split:
35 arrangement (converted), 45 content, 25 logic, 2 viewport, 1
body-class. Done.<br> or a
</tr><tr> row-break where desktop used
table cells. Each is split into the part that stays PHP and the part
that becomes CSS by emitting one markup shape — the desktop
cells — and adding a stack-narrow class whose
cells become display: block below 600px, so the row
stacks on a phone as the row-break used to. It covers the search form
(SearchformHelper, its $item_sep now a
constant cell break), the classifier and locale edit forms
(EditclassifierElement, EditlocalesElement),
and the group-users cell (ManagegroupsElement); the
submit-row and status spacers are always emitted and hidden narrow.
No MOBILE arrangement branch remains. Done.<meta name="viewport" content="width=device-width">
only when the request was sniffed as mobile, so a narrow screen that was
not sniffed kept a desktop-width viewport and never triggered the new
media queries. The gate is removed at WebLayout.php:100 and
at the upgrade page's own head in VersionFunctions.php, so
the viewport is always declared and the width, not the user agent,
decides the layout. With A's media queries in place this is safe; the
.mobile class rules still coexist until C retires them.
Implemented, waiting on review..mobile
class. 290 selectors depend on it. Whatever survives A and B
is either a real content decision or a rule that should have been a
media query. In substeps:
.mobile selectors that remain after A and B
and sort each into remove, fold into a media query, or keep as a
real content marker. The full list is in
IVC-mobile-selector-inventory.html:
246 rules across the four stylesheets (search.css, mail.css,
frise.css, messages.css). 243 fold into the existing
@media (max-width: 600px) block with the
.mobile prefix dropped — the media query already
means the narrow screen the class stood for — and 3 are
removals where a non-mobile sibling in the same rule already
applies. None are genuine content markers to keep. Implemented,
waiting on review..mobile X rule in
search.css, mail.css, and messages.css was folded into that file's
@media (max-width: 600px) block as X
(mail.css and messages.css had no such block, so one was made), and
the 3 redundant rules lost their .mobile selector. The
layout's inline body.mobile and
.mobile .top-landing-spacer styles became media
queries too, and the <body> no longer carries the
mobile class. A narrow-width render with the old class
and the new media query matched. The content and logic branches keep
reading the boolean $data['MOBILE'], which is a separate
value set earlier and untouched. Implemented, waiting on
review.Master to-do items 28 and 30. Both consume section I.
color-scheme: light dark, light-dark(),
prefers-color-scheme. Nothing in the stylesheets mentions
any of them today. This is not a theme in the
getThemeNames() sense — those are site-specific CSS
files that override Yioop's built-in styles (the V.D picker). A mode is
the built-in styles themselves carrying a second value for each named
color: both modes live in search.css, and the named colors from I.A get
a light value and a dark value through light-dark() so the
one stylesheet answers either. The toggle is got differently from a
theme file — the browser's prefers-color-scheme with
color-scheme on the root, not a linked override sheet. Needs
I.A first, since a mode is a second set of values for named colors and
there is nothing to name them against yet. A per-site switch gates it:
a checkbox in Appearance, stored as a new domain profile field beside
the existing color fields (SystemComponent's
APPEARANCE_DOMAIN_FIELDS, written by
ProfileModel::updateProfile, so no schema change), lets a
site turn light/dark on or off globally; off leaves the site in its
single built-in appearance as today. The field is given its default
through nsconddefine in Config after Profile.php loads, the
way the other profile-backed appearance fields are, so under the
persistent WebSite server the toggle takes effect from
p()'s cache on save rather than needing a restart on an
install whose profile predated the field. Implemented, waiting on
review.prefers-reduced-motion: reduce
block in search.css now cuts the animation and the transitions this
file sets to instant, the way mail.css already did for its one
spinner, so a visitor who asks the system for less motion is not given
the sliding panels or the spin. The aria-label hover
tooltips became :focus-visible as well as :hover
so a keyboard visitor gets the tooltip on tab focus while a mouse click
no longer flashes it. WCAG 2.2 AA contrast was checked with the ratio
formula on the landing and an admin page in both light and dark: every
text node meets its 4.5:1 (or 3:1 for large text) threshold. Left for
later, since AA already holds and each carries regression risk out of
proportion to the gain here: prefers-contrast and gating
the many :hover rules behind
@media (hover: hover). Implemented, waiting on review.ProfileModel::getThemeNames() and today
chosen per domain by an admin. This gives that choice to the visitor, in
the two places a visitor already changes how the site looks and reads:
SearchmenuElement, beside the language
element it already renders), and a matching one in a signed-in
user's account settings next to the language row
(ManageaccountElement, after the
options_element_language_label row). Both offer the
same <select> of getThemeNames().
Proposed.$_SESSION['THEME'] for a visitor who is not signed
in, following the SAFE_SEARCH and
OPEN_IN_TABS pattern in
SearchController, and for a signed-in user in the
per-user session blob the USER_SESSION table already
holds (UserModel::getUserSession /
setUserSession), where locale-style preferences already
live, so it follows them between devices with no schema change.
Proposed.WebLayout reads $data['PAGE_THEME_NAME']
to choose which theme stylesheet to link, falling back to the
domain's AUXILIARY_CSS_NAME. Feeding the visitor's pick
into PAGE_THEME_NAME (account value when signed in, else
the session value, else the domain default) makes it the visitor's
theme without a new emission path, and leaves a wiki page's own
theme, which sets the same field, untouched. Proposed.These were tried in Phase 4.5 on 2026-07-17 and put back: fixing them
there meant adding the .mobile branches item 23 removes and the
in units item 29 removes. They are small, they are the reason
this arc's order is what it is, and they are worth doing as soon as I and II
land, since they are also the proof that I and II were done right.
max-width: 600px alone, which a phone held sideways
never hit: it is wide (around 850px) but short, so it fell to the desktop
layout and the desktop min-width ran off the side and clipped the page.
The gate is now (max-width: 600px), (max-height: 480px), so
a short landscape phone gets the compact layout while a landscape tablet,
which stays well above 480px tall, keeps the desktop one. Measured, an
844×390 view went from overflowing at 1056px to fitting its 844px,
and portrait, a landscape tablet, and the desktop are unchanged. The
portrait toolbar can still fall to a second row when a long folder name
and every control will not fit the width together; holding it to one row
would mean shrinking the controls, so the wrap is left as it was.wiki.js with a heavy sixteen-pixel green border, a fixed
width fighting left and right insets, and a fixed height, so it looked
unlike any other dialog and was cut off on a phone. It now takes the
same shape as the standard light-content dialog: a rounded box with the
sidebar-color border, centered, sized to fit its content and held within
the viewport so it no longer runs off a phone. A poor line break in the
related Word Filter Preferences title — the bracketed Factory
Settings link splitting across two lines — is fixed in the same
pass by keeping the bracketed link together on one line. Waiting on
review.I before II (there is nothing to convert lengths into until the scale is named), I and II before IV (the tag is only safe once the layout can flex), I before V (a theme is a second set of values for named colors), and I and II before VI. III stands alone and can go at any point. IV.B is the last thing in the arc.